﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Dokan;
using System.IO;
using Logger;
using System.Diagnostics.Contracts;
namespace WindowsVHDClientNode
{

    /// <summary>
    /// A proof of concept example to show that the Dokan Net virtual Hard drive works
    /// </summary>
    public class SampleDrive : BaseDokanDrive
    {
        private static Log log = Log.Create<SampleDrive>();

        [ContractInvariantMethod]
        private void ObjectInvariant()
        {
            Contract.Invariant(_truncateRoot != null);
            Contract.Invariant(_truncateRoot.Length > 1);
            Contract.Invariant(_mountingPoint != null);
        }

        private DirectoryInfo _mountingPoint;
        private String _truncateRoot = null;

        /// <summary>
        /// The real directory to use as the root of this sample drive
        /// </summary>
        public DirectoryInfo MountingPoint
        {
            get { return _mountingPoint; }
            set
            {
                Contract.Requires(value != null && value.FullName.Length > 1);
                Contract.Ensures(_truncateRoot != null);

                _mountingPoint = value;
                _truncateRoot = _mountingPoint.FullName;                
                if (_truncateRoot.EndsWith(""+Path.DirectorySeparatorChar))
                {
                    _truncateRoot = _truncateRoot.Substring(0, _truncateRoot.Length - 1);
                }
                
                log.Debug("Changed mounting point to {0}", value);
                foreach (DirectoryInfo file in ListDirs(_mountingPoint, null))
                {
                    Contract.Assume(file != null);
                    log.Debug("D    {0}", StripRoot(file.FullName));
                }
                Contract.Assume(_mountingPoint != null);
                foreach (FileInfo file in ListFiles(_mountingPoint, null))
                {
                    Contract.Assume(file != null);
                    log.Debug("F    {0}", StripRoot(file.FullName));
                }

                Contract.Assume(_truncateRoot != null);
                Contract.Assume(_truncateRoot.Length > 1);
                Contract.Assume(_mountingPoint != null);
            }
        }

        private String StripRoot(String name)
        {
            Contract.Requires(name != null);
            return name.Replace(_truncateRoot, "").Trim(Path.DirectorySeparatorChar);
        }

        /// <summary>
        /// Constructs a sample drive
        /// </summary>
        public SampleDrive(char driveLetter, String volumeLabel, DirectoryInfo realMountingPoint)
            : base(driveLetter, volumeLabel)
        {
            Contract.Requires(volumeLabel != null);
            Contract.Requires(realMountingPoint != null && realMountingPoint.FullName.Length > 1);
            MountingPoint = realMountingPoint;
            Start();
        }

        private static IList<DirectoryInfo> ListDirs(DirectoryInfo dir, IList<DirectoryInfo> otarget)
        {
            Contract.Requires(dir != null);

            IList<DirectoryInfo> target = otarget ?? new List<DirectoryInfo>();
            target.Add(dir);
            foreach (DirectoryInfo subdir in dir.GetDirectories())
            {
                Contract.Assume(subdir != null);
                ListDirs(subdir, target);
            }
            return target;
        }

        private static IList<FileInfo> ListFiles(DirectoryInfo dir, IList<FileInfo> otarget)
        {
            Contract.Requires(dir != null);

            IList<FileInfo> target = otarget ?? new List<FileInfo>();
            foreach (FileInfo file in dir.GetFiles())
            {
                target.Add(file);
            }
            foreach (DirectoryInfo subdir in dir.GetDirectories())
            {
                Contract.Assume(subdir != null);
                ListFiles(subdir, target);
            }
            return target;
        }

        private FileSystemInfo getFile(String filename, DokanFileInfo info)
        {
            Contract.Requires(filename != null);
            Contract.Requires(info != null);

            String filenameNoLeadSlash = StripRoot(filename);
            try
            {
                log.Debug("Fetching file info: {0}", filenameNoLeadSlash);
                if (info.IsDirectory)
                {
                    return ListDirs(_mountingPoint, null).Where(dir => StripRoot(dir.FullName).Equals(filenameNoLeadSlash)).FirstOrDefault();
                }
                else
                {
                    return (FileSystemInfo)ListFiles(_mountingPoint, null).Where(file => StripRoot(file.FullName).Equals(filenameNoLeadSlash)).FirstOrDefault() ??
                        (FileSystemInfo)ListDirs(_mountingPoint, null).Where(dir => StripRoot(dir.FullName).Equals(filenameNoLeadSlash)).FirstOrDefault();
                }
            }
            catch (Exception e)
            {
                log.Error(e, "Could not fetch file info");
                throw;
            }
        }

        /// <summary>
        /// creates a file on the sample drive
        /// </summary>
        public override int CreateFile(String filename, FileAccess access, FileShare share,
            FileMode mode, FileOptions options, DokanFileInfo info)
        {
            Contract.Requires(filename != null);
            Contract.Requires(info != null);

            log.Debug("Create File: {0}", filename);
            return getFile(filename, info) != null ? 0 : -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        /// <summary>
        /// opens a directory on the sample drive
        /// </summary>
        public override int OpenDirectory(String filename, DokanFileInfo info)
        {
            Contract.Requires(filename != null);
            Contract.Requires(info != null);

            log.Debug("Open Directory: {0}", filename);
            return getFile(filename, info) != null ? 0 : -DokanNet.ERROR_PATH_NOT_FOUND;
        }

        /// <summary>
        /// The maximum length that can be read at once
        /// </summary>
        protected int _maxReadLength  = 65536;    

        /// <summary>
        /// reads a file from the sample drive
        /// </summary>
        public override int ReadFile(String filename, Byte[] buffer, ref uint readBytes,
            long offset, DokanFileInfo info)
        {
            Contract.Requires(filename != null);
            Contract.Requires(info != null);
            Contract.Requires(buffer != null);
            Contract.Requires(offset >= 0);

            try
            {
                log.Debug("Read File: {0}", filename);
                FileInfo file = (FileInfo)getFile(filename, info);
                if (file == null) { return -DokanNet.ERROR_FILE_NOT_FOUND; }
                using (Stream stream = file.OpenRead())
                {
                    int readlength = Math.Min((int)(file.Length - offset), Math.Min(buffer.Length, _maxReadLength));
                    Contract.Assume(readlength >= 0);
                    lock (stream)
                    {
                        stream.Position = offset;
                        readBytes = (uint)stream.Read(buffer, 0, readlength);
                    }
                    return 0;
                }
            }
            catch
            {
                return -1;
            }
        }

        /// <summary>
        /// Gets the file information for a given file
        /// </summary>
        public override int GetFileInformation(String filename, FileInformation fileinfo, DokanFileInfo info)
        {
            Contract.Requires(filename != null);
            Contract.Requires(fileinfo != null);
            Contract.Requires(info != null);

            log.Debug("GetFileInformation: {0}", filename);
            FileSystemInfo rfi = getFile(filename, info);
            if (rfi == null) { return info.IsDirectory ? -DokanNet.ERROR_PATH_NOT_FOUND : -DokanNet.ERROR_FILE_NOT_FOUND; }
            PopulateInfo(rfi, fileinfo);
            return 0;
        }

        private void PopulateInfo(FileSystemInfo rfi, FileInformation fileinfo)
        {
            Contract.Requires(rfi != null);
            Contract.Requires(fileinfo != null);

            fileinfo.FileName = rfi.Name;
            fileinfo.Attributes = FileAttributes.ReadOnly;
            if (rfi is DirectoryInfo)
            { fileinfo.Attributes |= FileAttributes.Directory; }
            fileinfo.CreationTime = rfi.CreationTime;
            fileinfo.LastAccessTime = rfi.LastAccessTime;
            fileinfo.LastWriteTime = rfi.LastWriteTime;
            fileinfo.Length = rfi is FileInfo ? ((FileInfo)rfi).Length : 0;
        }

        /// <summary>
        /// Lists the files present in the sample drive
        /// </summary>
        public override int FindFiles(String filename, IList<FileInformation> files, DokanFileInfo info)
        {
            Contract.Requires(filename != null);
            Contract.Requires(files != null);
            Contract.Requires(info != null);

            String filenameNoLeadSlash = StripRoot(filename);
            log.Debug("FindFiles: {0}", filenameNoLeadSlash);
            var results = from file in ListFiles(_mountingPoint, null)
                          where StripRoot(file.FullName).Contains(filenameNoLeadSlash)
                          select file;
            foreach (FileSystemInfo rfi in results)
            {
                Contract.Assume(rfi != null);
                log.Debug(" match: {0}", rfi.Name);
                FileInformation fi = new FileInformation();
                PopulateInfo(rfi, fi);
                files.Add(fi);
            }
            return 0;
        }

        /// <summary>
        /// Calculates the amount of free space available on the sample drive
        /// </summary>
        public override int GetDiskFreeSpace(ref ulong freeBytesAvailable, ref ulong totalBytes,
            ref ulong totalFreeBytes, DokanFileInfo info)
        {
            Contract.Requires(info != null);

            log.Debug("Get Free Disk Space");
            freeBytesAvailable = 0;
            totalFreeBytes = 0;
            return 0;
        }
    }
}
